package org.parngon.maze;

import java.awt.Point;

import java.awt.event.MouseEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseMotionListener;
import javax.swing.SwingUtilities;

import java.util.LinkedList;

public class MazeController extends MouseAdapter implements ComponentListener, MouseMotionListener, MazeListener
{
  AStar solver = new AStar();
  LinkedList <AStarNode>solution = null;

  Maze model = null;
  MazePanel view = null;
  boolean clicked = false;

  Point startDrag = null;
  boolean inDrag = false;

  Point lastSnap = null;

  public MazeController()
  {
    model = null;
    view = null;
  }

  public void setModel(Maze m)
  {
    if ( model != null ) {
      model.removeMazeListener(this);
    }

    model = m;
    model.addMazeListener(this);
  }

  public void setView(MazePanel mp)
  {
    if ( view != null ) {
      view.removeMouseListener(this);
      view.removeMouseMotionListener(this);
      view.removeComponentListener(this);
    }

    view = mp;
    view.addMouseListener(this);
    view.addMouseMotionListener(this);
    view.addComponentListener(this);
  }

  public void componentHidden(ComponentEvent ce)
  {
  }

  public void componentMoved(ComponentEvent ce)
  {
  }

  public void componentResized(ComponentEvent ce)
  {
    view.revalidate();
    view.setMazeSize(model.getSize());
    view.renderMazeImage(model);
    view.repaint();
  }

  public void componentShown(ComponentEvent ce) 
  {
  }

  Tile start = null;
  Tile end = null;

  Tile dragStarted = null;
  Tile dragCurrent = null;

  private Point findJoint(Point s)
  {
    int stepX = view.getWidth() / model.getWidth();
    int stepY = view.getHeight() / model.getHeight();
    int step = Math.min(stepX, stepY);

    int offsetX = (view.getWidth() - step * model.getWidth()) / 2;
    int offsetY = (view.getHeight() - step * model.getHeight()) / 2;

    int leftBound = offsetX;
    int rightBound = leftBound + step * model.getWidth();
    int topBound = offsetY;
    int bottomBound = topBound + step * model.getHeight();

    if ( s.x < leftBound || s.x >= rightBound || s.y < topBound || s.y >= bottomBound ) return null;

    int mx = (s.x - leftBound) / step;
    int my = (s.y - topBound) / step;

    int dx = s.x - leftBound - mx * step;
    int dy = s.y - topBound - my * step;

    int ss = (int)(step * 0.25);

    if ( (dx < ss || dx > step - ss) && (dy < ss || dy > step - ss) ) {
      return new Point(dx < ss ? mx : mx + 1, dy < ss ? my : my + 1);
    }

    return null;
  }

  private Tile findTile(Point s)
  {
    int stepX = view.getWidth() / model.getWidth();
    int stepY = view.getHeight() / model.getHeight();
    int step = Math.min(stepX, stepY);

    int offsetX = (view.getWidth() - step * model.getWidth()) / 2;
    int offsetY = (view.getHeight() - step * model.getHeight()) / 2;

    if ( s.x > offsetX && s.x < offsetX + step * model.getWidth() && s.y > offsetY && s.y < offsetY + step * model.getHeight() ) {
      int mx = (s.x - offsetX) / step;
      int my = (s.y - offsetY) / step;

      return model.getTile(mx, my);
    }

    return null;
  }

  public void mousePressed(MouseEvent me)
  {
    startDrag = me.getPoint();
  }

  public void mouseReleased(MouseEvent me)
  {
    if ( !inDrag ) {
      Point loc = me.getPoint();

      if ( !clicked ) {
        Tile s = findTile(loc);

        // set the start point
        if ( s != null ) {
          clicked = true;
          start = s;
          end = null;
          view.setStartTile(start);
          view.setEndTile(end);
          view.setSolutionPoints(null);
        }
      } else {
        Tile e = findTile(loc);

        // set the end point
        if ( e != null ) {
          clicked = false;
          end = e;
          view.setEndTile(end);
          solution = solver.findPath(start, end, model);
          if ( solution != null && solution.size() > 2 ) {
            LinkedList <Point>sps = new LinkedList<Point>();
            for ( AStarNode aNode: solution ) sps.add(aNode.getLocation());
            view.setSolutionPoints(sps);
          }
          requestViewRepaint();
        }
      }
    } else {
      inDrag = false;

      if ( lastSnap != null ) {
        Point currentSnap = findJoint(me.getPoint());
        if ( currentSnap != null ) {
/*
          if ( Math.abs(lastSnap.x - currentSnap.x) == 1 && lastSnap.y == currentSnap.y ) {
            Tile north = model.getTile(Math.min(lastSnap.x, currentSnap.x), lastSnap.y - 1);
            Tile south = model.getTile(Math.min(lastSnap.x, currentSnap.x), lastSnap.y);
            if ( north != null && south != null ) {
              if ( north.hasBottomWall() ) {
                model.breakWall(north, south);
              } else {
                model.buildWall(north, south);
              }
            }
          }
          if ( Math.abs(lastSnap.y - currentSnap.y) == 1 && lastSnap.x == currentSnap.x ) {
            Tile west = model.getTile(lastSnap.x - 1, Math.min(lastSnap.y, currentSnap.y));
            Tile east = model.getTile(lastSnap.x, Math.min(lastSnap.y, currentSnap.y));
            if ( west != null && east != null ) {
              if ( west.hasRightWall() ) {
                model.breakWall(west, east);
              } else {
                model.buildWall(west, east);
              }
            }
          }
*/
          int dx = Math.abs(lastSnap.x - currentSnap.x);
          int dy = Math.abs(lastSnap.y - currentSnap.y);
          if ( dx > 0 && dx < 5 && dy == 0 ) {
            Tile north, south;
            for ( int i = 0; i < dx; i++ ) {
              north = model.getTile(Math.min(lastSnap.x, currentSnap.x) + i, lastSnap.y - 1);
              south = model.getTile(Math.min(lastSnap.x, currentSnap.x) + i, lastSnap.y);
              if ( north != null && south != null ) {
                if ( north.hasBottomWall() ) {
                  model.breakWall(north, south);
                } else {
                  model.buildWall(north, south);
                }
              }
            }
          }
          if ( dy > 0 && dy < 5 && dx == 0 ) {
            Tile west, east;
            for ( int i = 0; i < dy; i++ ) {
              west = model.getTile(lastSnap.x - 1, Math.min(lastSnap.y, currentSnap.y) + i);
              east = model.getTile(lastSnap.x, Math.min(lastSnap.y, currentSnap.y) + i);
              if ( west != null && east != null ) {
                if ( west.hasRightWall() ) {
                  model.breakWall(west, east);
                } else {
                  model.buildWall(west, east);
                }
              }
            }
          }
        }
      }
      if ( dragStarted != null && dragCurrent != null && !dragStarted.equals(dragCurrent) ) {
        Point tlLoc = dragStarted.getLocation();
        Point brLoc = dragCurrent.getLocation();
        Point sLoc = new Point(Math.min(tlLoc.x, brLoc.x), Math.min(tlLoc.y, brLoc.y));
        Point eLoc = new Point(Math.max(tlLoc.x, brLoc.x), Math.max(tlLoc.y, brLoc.y));

        if ( me.getButton() == MouseEvent.BUTTON1 ) {
          model.addPattern(new RectangularArea(sLoc, eLoc, RectangularArea.BLOCK));
        } else if ( me.getButton() == MouseEvent.BUTTON3 ) {
          model.addPattern(new RectangularArea(sLoc, eLoc, RectangularArea.VOID));
        }
      }
//      view.setTopLeft(null);
//      view.setBottomRight(null);
//      requestViewRepaint();
    }
    view.setTopLeft(null);
    view.setBottomRight(null);
    view.setLastJoint(null);
    view.setCurrentJoint(null);
    requestViewRepaint();
  }

  public void mouseDragged(MouseEvent me)
  {
    Point cp = me.getPoint();

    inDrag = true;

    if ( lastSnap != null ) {
      view.setLastJoint(lastSnap);

      Point currentSnap = findJoint(cp);
      if ( currentSnap != null ) {
        int dx = Math.abs(lastSnap.x - currentSnap.x);
        int dy = Math.abs(lastSnap.y - currentSnap.y);
        if ( dx > 0 && dx < 5 && dy == 0 || dx == 0 && dy > 0 && dy < 5 ) {
          view.setCurrentJoint(currentSnap);
        } else {
          view.setCurrentJoint(null);
        }
      }
    } else {
      dragStarted = findTile(startDrag);
      dragCurrent = findTile(cp);

      view.setTopLeft(dragStarted);
      view.setBottomRight(dragCurrent);
    }

    requestViewRepaint();
  }

  public void mouseMoved(MouseEvent me)
  {
    if ( model == null ) return;

    Point cp = me.getPoint();
    Point currentSnap = findJoint(cp);
    view.setCurrentJoint(currentSnap);
    if ( currentSnap != lastSnap ) {
      requestViewRepaint();
      lastSnap = currentSnap;
    }
//System.err.println("mouseMoved: lastSnap = " + lastSnap + " currentSnap = " + currentSnap);

    Tile t = findTile(cp);
    if ( t != null ) {
      view.setToolTipText(t.toString() + (t.hasTopWall() ? "T" : "") + (t.hasBottomWall() ? "B" : "") + (t.hasLeftWall() ? "L" : "") + (t.hasRightWall() ? "R" : ""));
    } else {
      view.setToolTipText(null);
    }
  }

  public Tile getClosestTileInPath(Tile from)
  {
    if ( from == null ) return null;

    if ( start != null && end != null ) {
      solution = solver.findPath(start, end, model);
      if ( solution != null && solution.size() > 0 ) {
        double minCost = Double.MAX_VALUE;
        AStarNode minNode = null;
        for ( AStarNode aNode: solution ) {
          LinkedList <AStarNode>ts = solver.findPath(from, aNode, model);
          if ( ts.size() < minCost ) {
            minCost = ts.size();
            minNode = aNode;
          }
        }
        if ( minNode == null ) return null;

        return model.getTile(minNode.getLocation());
      }
    }

    return null;
  }

  public void mazeChanged(MazeEvent me)
  {
//    System.out.println(System.currentTimeMillis() + "=>mazeChanged()");
//    view.setStartTile(null);
//    view.setEndTile(null);
//    view.setSolutionPoints(null);
//    view.resetRobot();
    if ( start != null && end != null ) {
      solution = solver.findPath(start, end, model);
      if ( solution != null && solution.size() > 2 ) {
        LinkedList <Point>sps = new LinkedList<Point>();
        for ( AStarNode aNode: solution ) sps.add(aNode.getLocation());
        view.setSolutionPoints(sps);
//        view.adjustRobot();
        view.updateRobotLocation();
      } else {
        view.setSolutionPoints(null);
        view.resetRobot();
      }
    }

    if ( !view.isDisplayable() ) return;

    view.revalidate();
    Maze aMaze = (Maze)me.getSource();
    view.setMazeSize(aMaze.getSize());
    view.renderMazeImage(aMaze);

    requestViewRepaint();
  }

  private void requestViewRepaint()
  {
    SwingUtilities.invokeLater(
      new Runnable()
      {
        public void run()
        {
          view.repaint();
        }
      }
    );
  }
}
